import java.io.*;
import java.net.*;
import java.util.*;

/**
 * A server that keeps track of all game servers that are available on the
 * internet.
 *
 * If the client socket have been idle (no incoming data) for 30 seconds, it
 * will be disconnected and any game server added from that socket will be
 * removed.
 * The network protocol used to/from CentralServer is the following:
 *		get
 *			- Returns a list of all public game servers in the following format:
 *				list <number of servers>
 *			  And then one server per row after that in the following format:
 *				<address> <local address> <name>
 *		get <name>
 *			- Returns the address for a game server with a specific name, or
 *			  an empty line if such a game server does not exist. This game server
 *			  can be either private or public. The format returned is:
 *				list 1
 *				<address> <local address> <name>
 *			  or if the game wouldn't be found:
 *				list 0
 *		add <number of players> <public> <local address> <name>
 *			- Adds a game server to the list. The parameters are:
 *				<number of players> - The maximum number of players the
 *										game server allows.
 *				<public> - If equal to "public", the game server will be
 *							public to everyone, otherwise it will be considered
 *							a private game.
 *				<local address> - The local address of this game server.
 *				<name> - The name of the server, can contain spaces.
 *		remove
 *			- Forcefully removes the game server which have been created by this socket.
 *		ping
 *			- Pongs.
 */
public class CentralServer {
	/** A list of all open connections (threads).
	*/
	private ArrayList<ServerThread> openConnections;
	/** Contains information of all servers added, mapped by their addresses.
	* The format of the string array is the following:
	*	[name address nrOfPlayers privateGame localAddress]
	*/
	private HashMap<ServerThread, String[]> serverList; // address - info
	// fomat of string array: [name address nrOfPlayers privateGame localAddress]

	public static final int INFO_COUNT = 5;
	public static final int INFO_NAME = 0;
	public static final int INFO_ADDR = 1;
	public static final int INFO_NUMP = 2;
	public static final int INFO_PUBL = 3;
	public static final int INFO_LOCA = 4;
	
	/** The ServerSocket that listens for new connections.
	*/
	private ServerSocket serverSocket;

	public static void main(String args[]) {
		if (args.length < 1) {
			args = new String[1];
			args[0] = "5399";
		}
		if (args.length < 1) {
			System.err.println("Usage: java CentralServer <port>");
			System.exit(1);
		}
		CentralServer cs = new CentralServer(Integer.parseInt(args[0]));
	}

	/**
	* Creates a new CentralServer bound to a specific port.
	*
	* @param port	port number that the ServerSocket will get bound to
	*/
	public CentralServer(int port) {
		openConnections = new ArrayList<ServerThread>();
		serverList = new HashMap<ServerThread, String[]>();
		try {
			serverSocket = new ServerSocket(port);
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		}

		mainLoop();
	}

	/**
	* Adds a new game server with corresponding thread and info.
	*
	* @param serverThread	the thread that the socket is bound to
	* @param serverInfo		contains all the info the central server needs
	*						from this server. The format used is the same used
	*						for CentralServer.serverList
	* @return true if the game server was successfully added
	*/
	public boolean addGameServer(ServerThread serverThread, String[] serverInfo) {
		if (serverList.containsKey(serverThread.getAddresses())) {
			System.out.println("Server from the same address already exists");
			return false;
		}
		serverList.put(serverThread, serverInfo);
		System.out.println("New server added: "+Arrays.toString(serverInfo));
		return true;
	}

	/**
	* Returns a list of all public game servers known.
	*
	* @return list of game servers in the format: <global address> <local address> <name>
	*/
	public ArrayList<String> getGameServers() {
		ArrayList<String> list = new ArrayList<String>();
		Set<Map.Entry<ServerThread, String[]>> set = serverList.entrySet();
		Iterator<Map.Entry<ServerThread, String[]>> it = set.iterator();
		while (it.hasNext()) {
			Map.Entry<ServerThread, String[]> entry = it.next();
			
			String[] info = entry.getValue();
			if (info[INFO_PUBL].equals("public")) {
				list.add(info[INFO_ADDR] + " " + info[INFO_LOCA] + " " + info[INFO_NAME]);
			}
		}
		return list;
	}

	/**
	* Returns all information for a game server with a specific name, this
	* server can be either public or private. This method is case sensitive.
	*
	* @param name	name of the server
	* @return		list of information for the server.
	*/
	public String[] getServer(String name) {
		ArrayList<String> list = new ArrayList<String>();
		Set<Map.Entry<ServerThread, String[]>> set = serverList.entrySet();
		Iterator<Map.Entry<ServerThread, String[]>> it = set.iterator();
		while (it.hasNext()) {
			Map.Entry<ServerThread, String[]> entry = it.next();
			
			String[] info = entry.getValue();
			if (info[INFO_NAME].equals(name)) {
				return info;
			}
		}
		return null;
	}

	/**
	* Infinite loop that will listen to new connections. It will never end.
	*/
	public void mainLoop() {
		System.out.println("Awaiting connections...");
		
		while (true) {
			try {
				Socket socket = serverSocket.accept();
				System.out.println("Client connected");
				ServerThread st = new ServerThread(this, socket);
				openConnections.add(st);
				st.start();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	* Removes a game server from the list of avaliable servers.
	*
	* @param serverAddress	the global address for the server to be removed
	*/
	public void removeGameServer(ServerThread server) {
		if (serverList.containsKey(server)) {
			serverList.remove(server);
			System.out.println("Server '"+server.getAddresses()+"' removed");
		}
	}
}


/**
* Keeps the connection to a client alive.
*/
class ServerThread extends Thread {
	/** Reference back to the CentralServer that created this ServerThread
	*/
	private CentralServer centralServer;
	/** Socket to the client connected
	*/
	private Socket socket;
	/** Scanner bound to the sockets InputStream
	*/
	private EasySocket easySocket;
	private Scanner in;
	/** PrintStream bound to the sockets OutputStream
	*/
	private PrintStream out;
	/** The timeout left before the connection is interrupted
	*/
	private int timeout = 30000;
	/** The local address reported by the client
	*/
	private String localAddress = "";

	/**
	* Creates a new ServerThread connected to a socket.
	*
	* @param centralServer	reference to the CentralServer that creates this instance
	* @param socket			socket to the client
	*/
	public ServerThread(CentralServer centralServer, Socket socket) {
		this.centralServer = centralServer;
		this.socket = socket;
		easySocket = new EasySocket(socket);
		/*try {
			in = new Scanner(socket.getInputStream());
			out = new PrintStream(socket.getOutputStream());
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		}*/
	}

	/**
	* Resets the timeout for this client to 30 seconds.
	*/
	public void resetTimeout() {
		timeout = 30000; // 30000 ms
	}

	/**
	* Returns the address for the socket used,
	*
	* @return string representation of the address
	*/
	public String getAddress() {
		return socket.getInetAddress().getHostAddress();
	}

	/**
	* Returns the addresses for the socket used (both the global address reported
	* by the socket, and the local address reported by the client).
	*
	* @return string representation of the address
	*/
	public String getAddresses() {
		return getAddress() + "_" + localAddress;
	}

	/**
	* Main loop for listening to the client. Protocol used is specified in the help
	* for CentralServer.
	*/
	public void run() {
		System.out.println("New thread started");
		//try {
		while (timeout > 0) {
			while (easySocket.hasNextLine()) {
				String line = easySocket.nextLine();
				System.out.println("Incoming: "+line);
				resetTimeout();

				if (line.equals("get")) {
					ArrayList<String> list = centralServer.getGameServers();
					String outStr = "list "+list.size();
					for (String str : list) {
						System.out.println("--- "+str);
						outStr += "\n"+str;
					}
					easySocket.println(outStr);
				} else if (line.startsWith("get ")) {
					String[] parts = line.split(" ", 2); // Split into two chunks, "get" and the rest
					if (parts.length == 2) {
						String[] info = centralServer.getServer(parts[1]);
						if (info != null) {
							easySocket.println("list 1\n"+info[CentralServer.INFO_ADDR] + " " + info[CentralServer.INFO_LOCA]);
						} else {
							easySocket.println("list 0");
						}
					}
				} else if (line.startsWith("add ")) {
					String[] parts = line.split(" ", 5);
					if (parts.length == 5) {
						String[] info = new String[CentralServer.INFO_COUNT];

						info[CentralServer.INFO_NUMP] = parts[1];
						info[CentralServer.INFO_PUBL] = parts[2];
						info[CentralServer.INFO_LOCA] = parts[3];
						info[CentralServer.INFO_NAME] = parts[4];
						
						info[CentralServer.INFO_ADDR] = getAddress();

						localAddress = parts[3];

						centralServer.addGameServer(this, info);
					}
				} else if (line.equals("remove")) {
					centralServer.removeGameServer(this);
				} else if (line.equals("ping")) {
					easySocket.println("pong");
				}
			}


			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			timeout -= 100;
		}
		//} catch (IOException e) {
		//	e.printStackTrace();
		//} finally {
		//	System.out.println("Client timeout");
		//}
		System.out.println("Client timeout");
		centralServer.removeGameServer(this);
		easySocket.close();
	}
}
